package P2POperational;

import Commands.Command;
import Commands.InstructionListener;
import Commands.InstructionPack;
import Commands.TerminalCommand;
import P2PConnection.ConnectionHandler;
import P2PConnection.DownloadListener;
import Terminal.TerminalListener;

import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;


/**
 *  Created with IntelliJ IDEA.
 *  Authors: Henrique Alberto Rusa and Danilo BArbosa Porto
 *  Date: 04/11/13
 *
 *  LocalOperational class implements InstructionListener abd TerminalListener interfaces; so it reside on the local
 *  where all the operations are going to be realized, listening to received instructions from the connectionHandler
 *  and listening to commands from the terminal.
 *  The class will keep a table where all the files form the system are referred, so every time it is required to
 *  use a file, even if the node does not have the data, it will acquire and execute the command.
 */
public class LocalOperational implements InstructionListener, TerminalListener, DownloadListener, Runnable {

    /*
     *  List of the commands received from the ConnectionHandler when an event of receiving occurred
     */
    private List<Command> queue;

    /*
     *  Table of Contents where our references to files and nodes are placed.
     *  It will be used to give consistency to our file system.
     */
    private Map<File,InetAddress> tableOfContents;

    /*
     *  Path parameter that will reference the root of the file system on the node where the application is running.
     */
    private Path rootDirectory;

    /*
     *  parameter Path that will reference the full path of the newly created file when an watcher event occurs.
     */
    private Path fullPath;

    /*
     * InetAddres to reference the local address, and be reference for others node of the application
     */
    private InetAddress myAddress;

    /*
     * boolean for the control of the loop of run method, used to end processing
     */
    private boolean control;

    /*
     * Layer of connection that will be used to remote access the filesystem
     */
    private ConnectionHandler remoteLayer;

    /*
     *  Parameter that will be used to implement the events and processes of the central node instead of the
     *  regular node operation.
     */
    private boolean isCentralNode;

    /*
     *  String to identify own name on addresses.
     */
    private String myName;

    /*
     *  Constructor of the class.
     *  This method will allocate the instances simply by using an identifier of type string, where when receiving a
     *  directory, it will allocate a LocalOperational for a cluster node, and when receiving a string "central" it will
     *  allocate a central node localOperational.
     */
    public LocalOperational(String onCreate, String name) throws InputMismatchException{

        // Getting root directory by file to path
        File temp = new File(onCreate);
        Path tempDirectory = temp.toPath();
        this.rootDirectory = tempDirectory;
        fullPath = rootDirectory;
        remoteLayer = new ConnectionHandler(this);
        control = true;

        try {
            myAddress = InetAddress.getByName(myName);
        } catch (UnknownHostException e) {
            //tODO
        }

        // Instantiating the list of instructions and the map of files.
        queue = Collections.synchronizedList(new LinkedList<Command>());
        tableOfContents = Collections.synchronizedMap(new HashMap<File, InetAddress>());

        // adds the operational as listener to remote layer and starts the layer
        remoteLayer.addListener(this);
        new Thread(remoteLayer).start();

        // getting name
        this.myName = name;

        // Instantiating according to String.
        if(onCreate.equals("Central")){
            this.isCentralNode = true;
        }
        else if (temp.isDirectory()) {
            this.isCentralNode = true;
        } else{
            // if parameters are not like expected, will throw a MismatchException.
            throw new InputMismatchException();
        }
    }

    /*
     * Method to implement the locked remove from the queue, so no deadlocks and starvation can appear on our
     * problem.
     */
    private synchronized InstructionPack removeFromQueue(){
        InstructionPack command;
        synchronized (queue) {
            command = (InstructionPack)queue.remove(0);
        }
        return command;
    }

    /*
     * Method that at the beginning of the filesystem, the central server will listen to a enable command from any client
      * than it will start to merge table sent to it, and at the end, return the tables to the clients.
     */
    private void synchronizeNodes(){
        synchronized (tableOfContents){
            this.scanFilesOnDirectories(this.fullPath.toFile());
            remoteLayer.updateTable(this.tableOfContents);
        }
    }

    /*
     *  Overriding method of instruction listener.
     *  This will implement the action when the connection handler receives a new instruction on the it´s list.
     *  The instruction received will be passed to LocalOperational through the Command on the event.
     */
    @Override
    public void hasNewInstruction(Command onEvent) {
        InstructionPack command = (InstructionPack)onEvent;
        this.placeOrder(command);
    }

    /*
     *  Method to verify if the node is a central one or not.
     *  Is synchronized to prevent thread deadlock and starvation.
     */
    private synchronized boolean isCentralNode(){
        return isCentralNode;
    }

    /*
     *  Setter of the tableOfContents, this will be synchronized to prevent deadlocks or starvation.
     */
    public synchronized void setTableOfContents(Map<File, InetAddress> tableOfContents) {
        this.tableOfContents = tableOfContents;
    }

    /*
     *  Merger of the tableOfContents, this will be synchronized to prevent deadlocks or starvation.
     */
    public synchronized void mergeTableOfContents(Map<File, InetAddress> tableOfContents) {
        this.tableOfContents.putAll(tableOfContents);
    }

    /*
         *  Method to place an order on the list of future processed commands on the localOperational.
         *  This method will be used together with the implementation of the terminal listener
         */
    public synchronized void placeOrder(Command received) {
        synchronized (queue) {
            queue.add(received);
        }
    }

    /*
     *  Overriding method that implements the terminal listener interface. Calling it is the result of an terminal event
     *  where the command will be passed as the parameter. The method will verify what kind of command it is and put it
     *  on the list o commands, where the execution of the object will verify if it has a empty list, or else process the
     *  command.
     */
    @Override
    public void onCommandType(Command evt) {

        // casting type of command to correct object used
        TerminalCommand typed = (TerminalCommand)evt;

        // Selection of what type of command it is and calling it´s methods to implement the process
        if (TerminalCommand.isCreateCommand(evt)){
            this.create(typed.getFileName());
            return;
        } else if (TerminalCommand.isListCommand(evt)){
            this.list();
            return;
        } else if (TerminalCommand.isReadCommand(evt)){
            this.read(typed.getFileName());
            return;
        } else if (TerminalCommand.isRemoveCommand(evt)){
            this.remove(typed.getFileName());
            return;
        } else if (TerminalCommand.isWriteCommand(evt)){
            this.write(typed.getFileName());
            return;
        } else if (TerminalCommand.isClearCommand(evt)){
            this.clear();
            return;
        } else if (TerminalCommand.isMkdirCommand(evt)){
            this.mkdir(typed.getFileName());
            return;
        } else if (TerminalCommand.isCDCommand(evt)) {
            this.cd(typed.getFileName());
            return;
        } else if (TerminalCommand.isEnableCommand(evt)) {
            if(isCentralNode) {
                this.synchronizeNodes();
                System.out.print(fullPath.toString() + " # ");
            }
            return;
        }else if (TerminalCommand.isRmdirCommand(evt)){
            this.removeDirectory(typed.getFileName());
            return;
        }else if (TerminalCommand.isExitCommand(evt)){
            this.exit();
            return;
        }
    }

    /*
     *  Overriding method of the TerminalListener, where we are implementing the list command event on
     *  our terminal output.
     */
    @Override
    public void list() {
        synchronized (tableOfContents){
            /*
             * Gets iterator of the hashtable key and verify who has the parent as the folder that the user is
             * currently in.
             */
            Iterator it = tableOfContents.keySet().iterator();
            while(it.hasNext()){
                // casts file type to element
                File element = (File) it.next();
                // verifying if element is on the current path of user
                if (element.getParent().equals(fullPath.toString())){
                    System.out.print(" " + element.getName() + " ");
                }
            }
        }
        // Always update table when on a ls command
        remoteLayer.updateTable(null);
        System.out.print("\n" + fullPath + " # ");
    }

    /*
     *  Overriding method of the TerminalListener, where we are implementing the create command event on
     *  our terminal output.
     */
    @Override
    public void create(String filename) {
        // Assembles the file path considering where the user is in the system, and creates the file.
        synchronized (tableOfContents) {
            Path path = Paths.get(fullPath.toString() + "/" + filename);
            final File arq = path.toFile();
            try {
                if(!isCentralNode) {
                    arq.createNewFile();
                    remoteLayer.sendCreateToCentral(arq, myAddress);
                }
                tableOfContents.put(arq,myAddress);
            } catch (IOException e) {
                //TODO
            }
        }
        System.out.print(fullPath + " # ");
    }

    /*
     *  Overriding method of the TerminalListener, where we are implementing the remove command event on
     *  our terminal output.
     */
    @Override
    public void remove(String filename) {
        synchronized (tableOfContents){

            // We will create a file and search for it inside the table
            Path filePath = Paths.get(fullPath.toString() + "/" + filename);
            File arq = filePath.toFile();

            if(!isCentralNode) {
                if(tableOfContents.containsKey(arq) && arq.isFile()) {
                    if(!tableOfContents.get(arq).equals(myAddress)) {
                        // send a remove to the corresponding node that has the file
                        remoteLayer.sendRemove(arq,tableOfContents.get(arq));
                    }else{
                        // the file is in the node directory, delete it
                        try {
                            tableOfContents.remove(arq);
                            Runtime.getRuntime().exec("rm " + arq.getName());
                        } catch (IOException e) {
                            //TODO
                        }
                    }
                }
            }
            else {
                if(tableOfContents.containsKey(arq)){
                    tableOfContents.remove(arq);
                }
            }
        }
        System.out.print(fullPath + " # ");
    }

    /*
     * implemented method of the terminal listener, where we have to implement the remove directory from our filesystem
     * and from our table of contents
     */
    @Override
    public void removeDirectory(String folderName) {
        synchronized (tableOfContents){
            Path path = Paths.get(fullPath.toString() + "/" + folderName);
            File arq = path.toFile();

            if(tableOfContents.containsKey(arq)) {
                if(!isCentralNode && arq.isDirectory()) {
                    if(!tableOfContents.get(arq).equals(myAddress)) {
                        // send a remove to the corresponding node that has the file
                        remoteLayer.sendRemove(arq,tableOfContents.get(arq));
                    }
                    else {
                        // if file arq is in this node, send remove to central node
                        remoteLayer.sendRemove(arq);
                    }
                }
                else if (arq.isDirectory()){
                    File[] files = arq.listFiles();

                    for (File element : files){
                        try {
                            tableOfContents.remove(element);
                            Files.delete(Paths.get(element.getAbsolutePath()));
                        } catch (IOException e) {
                            //TODO
                        }
                    }
                }
                tableOfContents.remove(arq);
            }
        }
        System.out.print(fullPath.toString() + " # ");
    }

    /*
     *  Overriding method of the TerminalListener, where we are implementing the read command event on
     *  our terminal output.
     */
    @Override
    public void read(String filename) {
        File arq = new File(filename);
        if(arq.exists()){
            System.out.println(" Arquivo pronto para leitura! ");
        }
        else{
            synchronized (tableOfContents){
                try {
                    remoteLayer.sendExchange(tableOfContents.get(arq),filename);
                    remoteLayer.updateTable(null);
                } catch (IOException e) {
                    //TODO
                }
            }
        }
    }

    /*
     *  Overriding method of the TerminalListener, where we are implementing the write command event on
     *  our terminal output.
     */
    @Override
    public void write(String filename) {
        File arq = new File(filename);
        if(arq.exists()){
            System.out.println(" Arquivo pronto para escrita! ");
        }
        else{
            synchronized (tableOfContents){
                try {
                    remoteLayer.sendExchange(tableOfContents.get(arq),filename);
                    remoteLayer.updateTable(null);
                } catch (IOException e) {
                    //TODO
                }
            }
        }
    }

    /*
     *  Overriding method of the TerminalListener, where we are implementing the clear command event on
     *  our terminal output.
     */
    @Override
    public void clear() {
        // Call the linux system command clear
        System.out.println("\n\n\n\n\n\n\n\n\n\n\n\n" +
                "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
                "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
                "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
        System.out.print(fullPath.toString() + " # ");
    }

    /*
     * Implemented method from the terminal listener where we have to implement the cd command, to navigate through
     * the directories of the application
     */
    @Override
    public void cd(String folderName) {
        // Assembles the path on the variable when the user goes into and out the folders
        if(folderName.equals("..")){
            // get parent path when user returns the folder
            if(!fullPath.equals(rootDirectory)){
                fullPath = fullPath.getParent();
            }
        }
        else {
            // creates new path with ongoing directory
            String path = fullPath.toString() + "/" + folderName;

            // creates a file to verify existence and if it is a directory
            File dir = new File(path);

            if(dir.exists() && dir.isDirectory())
                this.fullPath = Paths.get(path);
        }
        System.out.print(fullPath.toString() + " # ");
    }

    /*
     * Implemented method to execute the mkdir on the terminal and on the filesystem. we will update our table and
     * the filesystem utilized with the current file.
     */
    @Override
    public void mkdir(String folderName) {
        synchronized (tableOfContents) {

            // creates a directory
            Path path = Paths.get(fullPath.toString() + "/" + folderName);
            final File dir = new File(path.toString());
            dir.mkdir();

            // Adds the directory to the hash table
            tableOfContents.put(dir, myAddress);
            remoteLayer.sendCreateToCentral(dir,myAddress);
        }
        System.out.print(fullPath.toString() + " # ");
    }

    /*
     * Implemented method to execute the exit method on the terminal, closing all the instances of the filesystem
     * that are running.
     */
    @Override
    public void exit() {
        remoteLayer.setAlive(false);
        this.control = false;
    }

    /*
     *  Call for the search all directories method.
     *  It verifies if the File directory is really a directory, then proceeds to start the search, verifying all
     *  files and folders it it happens to exists.
     */
    private void scanFilesOnDirectories(File directory) {
        if(directory.isDirectory()) {

            // refactor the hash table when it calls the method
            tableOfContents.clear();

            // if it is a directory, search, else wo´nt do anything
            search(directory);
        }
    }

    /*
     *  Method to search for all files in the directoryToSearch root folder. It will go down the entire tree to
     *  find all the files. When it finds a file, the file is added to the current hash. In the end of the search
     *  method, the user will have a hash with all the files in the directory
     */
    private void search(File directoryToSearch) {

        // list all files and directory files of the path
        File[] tempFileQueue = directoryToSearch.listFiles();

        if(directoryToSearch.isDirectory()) {
            // if directory is readable
            if(directoryToSearch.canRead()) {
                // verify all the files in the queue of files (files and directory files)
                for(File element : tempFileQueue) {

                    if(element.isDirectory()) {
                        synchronized (tableOfContents) {
                            tableOfContents.put(element,myAddress);
                        }
                        search(element);
                    }
                    else {
                        synchronized (tableOfContents) {
                            tableOfContents.put(element,myAddress);
                        }
                    }
                }
            }
        }
    }

    /*
     * Override method from the interface download listener where we will
     */
    @Override
    public void finishedDownload(File evt) {
        synchronized (tableOfContents){
            tableOfContents.put(evt, myAddress);
        }
    }

    /*
     *  Method run to keep the connection listening to the instructions on the line, processing them when there is
     *  a new one allocated with the servers.
     *  This call will verify all the kinds of instructions to choose the correct action to take.
     */
    @Override
    public void run() {
        while(control){
            InstructionPack command;
            if(!queue.isEmpty()) {
                command = this.removeFromQueue();

                if (command.isUpdateTableInstruction()){
                    if (isCentralNode()){
                        // On central node, the code merges all the files on the table
                        mergeTableOfContents(command.getTable());
                    }
                    else {
                        // Getting new map from package
                        setTableOfContents(command.getTable());
                    }
                }
                else if (command.isDeleteInstruction()){
                    File arq = command.getFilePath();
                    if(arq.isFile()){
                        remove(command.getFileName());
                    } else if (arq.isDirectory()){
                        removeDirectory(command.getFileName());
                    }

                }
                else if (command.isExchangeInstruction()){
                    // When some node is requesting a file, just creates a FTPClient to send it, passing the file name and host.
                    try {
                        synchronized (tableOfContents){
                            remoteLayer.sendFile(tableOfContents.get(command.getFilePath()),command.getFilePath().getName());
                        }
                    } catch (IOException e) {
                        //TODO
                    }

                } else if (command.isCreateInstruction()){
                /*
                 *  This call will have two uses. First it will only work on central server, when a node creates a file
                 *  and sends te central the information just to update the table. Second, when something is altered, we
                 *  will make a create instruction, so the table will over write the key, putting the most recent file on it.
                 */
                    if(isCentralNode)
                        this.mergeTableOfContents(command.getTable());
                }
            }
        }
    }
}


























































































